package com.dana1.nos.old.impl;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.ClassUtils;

import com.dana1.nos.old.ObjectKey;
import com.dana1.nos.old.ObjectService;
import com.dana1.nos.old.ServiceObject;
import com.dana1.nos.old.meta.KeyField;
import com.dana1.nos.old.util.HardMemoizer;
import com.dana1.nos.old.util.Producer;
import com.dana1.nos.old.util.PropertyUtils;

public abstract class AbstractObjectService implements ObjectService
{

	private boolean autoInsertServiceObjects = false;
	
	public boolean isAutoInsertServiceObjects()
	{
		return autoInsertServiceObjects;
	}
	public void setAutoInsertServiceObjects(boolean autoInsertServiceObjects)
	{
		this.autoInsertServiceObjects = autoInsertServiceObjects;
	}

	// search operations
	@Override public abstract <T extends ServiceObject> List<ObjectKey<T>> query(T obj);

	// passthru operation. The multiple sql statements are so that a true list of affected object keys can be returned by the underlying driver
	@Override public abstract List<ObjectKey<?>> execute(Map<String,Object> parameters, String ... sql);

	// basic m-m relation operators
	@Override public abstract List<ObjectKey<?>> retrieveMM(ObjectKey<? extends ServiceObject> ownerKey,String relationName);
	@Override public abstract int countMM(ObjectKey<? extends ServiceObject> ownerKey,String relationName);
	@Override public abstract boolean addMM(ObjectKey<? extends ServiceObject> ownerKey, String relationName, ObjectKey<? extends ServiceObject> childKey);
	@Override public abstract boolean removeMM(ObjectKey<? extends ServiceObject> ownerKey, String relationName, ObjectKey<? extends ServiceObject> childKey);

	// basic 1-m relation operators
	// @Override public abstract ObjectKey<? extends ServiceObject> retrieveM1(ObjectKey<? extends ServiceObject> parentKey,String relationName);
	@Override public abstract <T extends ServiceObject> List<ObjectKey<T>> retrieve1M(ObjectKey<?> childKey, Class<T> parentObjectClass, String relationName);
	@Override public abstract int count1M(ObjectKey<? extends ServiceObject> childKey, Class<? extends ServiceObject> parentObjectClass, String relationName);
	@Override public abstract boolean add1M(ObjectKey<? extends ServiceObject> childKey, String relationName, ObjectKey<? extends ServiceObject> parentKey);
	@Override public abstract boolean remove1M(ObjectKey<? extends ServiceObject> childKey, String relationName, ObjectKey<? extends ServiceObject> parentKey);

	// basic 1-1 relation operators
	// @Override public abstract ObjectKey<? extends ServiceObject> get11(ObjectKey<? extends ServiceObject> childKey,String relationName);
	@Override public abstract boolean set11(ObjectKey<? extends ServiceObject> leftKey, String relationName, ObjectKey<? extends ServiceObject> rightKey);

	
	// Object operations
	@SuppressWarnings("unchecked")
	@Override 
	public <T extends ServiceObject> ObjectKey<T> insert(T obj) 
	{
		return (ObjectKey<T>)insert((List)Arrays.asList(obj)).iterator().next(); 
	}
	@SuppressWarnings("unchecked")
	@Override public <T extends ServiceObject> T retrieve(ObjectKey<T> key)
	{
		return (T)retrieve((List)Arrays.asList(key)).iterator().next(); 
	}
	@Override public <T extends ServiceObject> void update(T obj)
	{
		update(Arrays.asList((ServiceObject)obj)); 
	}
	@SuppressWarnings("unchecked")
	@Override public <T extends ServiceObject> void delete(ObjectKey<T> key)
	{
		delete((List)Arrays.asList(key)); 
	}

	// Object operations
	@Override public List<ObjectKey<?>> insert(List<ServiceObject> objs)
	{
		ArrayList<ObjectKey<?>> results = new ArrayList<ObjectKey<?>>();
		for (ServiceObject obj: objs)
			results.add(insert(obj));
		return results;
	}
	@Override public List<ServiceObject> retrieve(List<ObjectKey<?>> keys)
	{
		ArrayList<ServiceObject> results = new ArrayList<ServiceObject>();
		for (ObjectKey<?> key: keys)
			results.add(retrieve(key));
		return results;

	}
	@Override public void update(List<ServiceObject> objs)
	{
		for (ServiceObject obj: objs)
			update(obj);
	}
	@Override public void delete(List<ObjectKey<?>> keys)
	{
		for (ObjectKey<?> key: keys)
			delete(key);
	}
	
	// object handling operations
	@SuppressWarnings("unchecked")
	@Override 
	public <T extends ServiceObject> T newInstance(Class<T> iObjClass) 
	{ 
		T obj = createBlankServiceObject(iObjClass);
		((ServiceObjectInvocationHandler)Proxy.getInvocationHandler(obj)).setGeneratedFields(obj);
		((ServiceObjectInvocationHandler)Proxy.getInvocationHandler(obj)).setAutoInsertServiceObjectProperties(autoInsertServiceObjects);
		return obj;
	}

	@SuppressWarnings("unchecked")
	@Override 
	public <T extends ServiceObject> ObjectKey<T> objectKey(T obj)  
	{
		try
		{
			Class<T> serviceObjClass = getServiceObjectClass(obj);
			Map<String,Comparable<?>> locals = new HashMap<String,Comparable<?>>();
			for (PropertyDescriptor prop : keyFieldDescriptors(serviceObjClass))
				extractObjectKeyFields( prop, obj, serviceObjClass, locals);
			ObjectKey<T> okey = new ObjectKey<T>(serviceObjClass,locals);
			if (!((ServiceObjectInvocationHandler)Proxy.getInvocationHandler(obj)).isConnected())
				okey.setServiceObject(obj);
			return okey;
		}
		catch (Exception e)
		{
			if (e instanceof RuntimeException)
				throw (RuntimeException)e;
			throw new RuntimeException(e.getMessage(),e);
		}
	}

	
	@SuppressWarnings("unchecked")
	protected static final <T extends ServiceObject> ServiceObjectInvocationHandler<T> handlerFor(T obj)
	{
		return (ServiceObjectInvocationHandler)Proxy.getInvocationHandler(obj);
	}

	@SuppressWarnings("unchecked")
	private <T extends ServiceObject> T createBlankServiceObject(Class<T> iObjClass)
	{
		Class [] allInterfaceNames = (Class[]) ClassUtils.getAllInterfaces(iObjClass).toArray(new Class[0]);
		allInterfaceNames = (Class [])ArrayUtils.add(allInterfaceNames, 0, iObjClass);
		return (T)Proxy.newProxyInstance(iObjClass.getClassLoader(), allInterfaceNames, new ServiceObjectInvocationHandler(this,iObjClass));
	}

	private PropertyDescriptor [] keyFieldDescriptors(final Class<? extends ServiceObject> serviceObjClass)
	{
		return HardMemoizer.memoize(new Producer<PropertyDescriptor []>() {
			@Override
			public PropertyDescriptor[] produce()
			{
				ArrayList<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();
				for (PropertyDescriptor prop : PropertyUtils.getPropertyDescriptors(serviceObjClass,Object.class))
					if (null!=getGettorAnnotation(serviceObjClass, prop, KeyField.class))
						result.add(prop);
				return result.toArray(new PropertyDescriptor[0]);
			}
		}, AbstractObjectService.class,serviceObjClass,"keyFieldDescriptors");
	}
	
	@SuppressWarnings("unchecked")
	protected <T extends ServiceObject> Class<T> getServiceObjectClass(T obj)
	{
		Class<T> objClass = (Class<T>)obj.getClass();
		if (Proxy.isProxyClass(obj.getClass()))
			objClass= ((ServiceObjectInvocationHandler<T>)Proxy.getInvocationHandler(obj)).getProxyClass();
		return objClass;
	}
	
	private <T extends Annotation> T getGettorAnnotation(Class<? extends ServiceObject> proxyClass, PropertyDescriptor propDesc, Class<T> annoClass)
	{
		try
		{
			return  proxyClass.getMethod(propDesc.getReadMethod().getName(),new Class<?>[0]).getAnnotation(annoClass);
		}
		catch (Exception e)
		{
			throw new RuntimeException(e.getMessage(),e);
		}
	}

	private void extractObjectKeyFields(PropertyDescriptor prop, ServiceObject obj, Class<? extends ServiceObject> serviceObjClass, Map<String, Comparable<?>> locals ) throws IllegalAccessException, InvocationTargetException
	{
		if (Collection.class.isAssignableFrom(prop.getPropertyType()))
			throw new RuntimeException("KeyField declared on collection type in "+ obj.getClass().getName());

		if (ServiceObject.class.isAssignableFrom(prop.getPropertyType()))
			includeServiceObjectKeyForProperty(prop,objectKey(actualOrBlank(prop,obj)), locals);
		else
			locals.put(prop.getName(),(Comparable<?>)prop.getReadMethod().invoke(obj));
	}

	@SuppressWarnings("unchecked")
	private ServiceObject actualOrBlank(PropertyDescriptor prop, ServiceObject parent) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException
	{
		ServiceObject obj = (ServiceObject)prop.getReadMethod().invoke(parent);
		if (obj==null)
			obj = createBlankServiceObject((Class<? extends ServiceObject>)prop.getPropertyType());
		return obj;
	}
	
	private void includeServiceObjectKeyForProperty(PropertyDescriptor prop, ObjectKey<?> objectKey, Map<String, Comparable<?>> locals) 
	{
		for (Map.Entry<String, Comparable<?>> me: objectKey.getKeyValues().entrySet())
			locals.put(prop.getName()+"_"+me.getKey(),me.getValue());
	}
	protected <T extends ServiceObject> T retrieveOrCheat(ObjectKey<T> objectKey)
	{
		T obj = objectKey.getServiceObject();
		if (obj==null)
			return retrieve(objectKey);
		if (handlerFor(obj).isConnected())
			return retrieve(objectKey);
		return obj;
	}

}
